import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:http/http.dart' as libHttp;
import 'package:http/http.dart';
import 'package:jet_rails/jr_constant/jr_urls.dart';
import 'package:jet_rails/jr_utils/jr_device.dart';
import 'package:jet_rails/jr_utils/jr_logger.dart';

class JrHttp {
  JrHttp({this.baseURL = JrUrls.baseUrl});
  static const String TAG = 'JrHttp';
  static const Duration connectionTimeout = Duration(seconds: 20);

  String baseURL;

  String? accessToken;

  Future<T> get<T>(
    String path, {
    Map<String, String>? headers,
    Map<String, dynamic>? query,
  }) async {
    String url = baseURL + path;
    Map<String, String> mergedHeaders = _mergeHeaders(headers);
    Uri uri = Uri.parse(url).replace(queryParameters: _stringifyValue(query ?? {}));
    Future<Response> requestFuture = libHttp.get(uri, headers: mergedHeaders);
    Response response = await _requestWrapper(requestFuture);
    return _responseWrapper(response);
  }

  Future<T> post<T>(String path,
      {Map<String, String>? headers, Map<String, dynamic>? query, Object? body, bool wrap = true}) async {
    String url = baseURL + path;
    Map<String, String> mergedHeaders = _mergeHeaders(headers);
    if (mergedHeaders['Content-Type'] == null) {
      mergedHeaders['Content-Type'] ??= 'application/json';
      body = json.encode(body);
    }
    Uri uri = Uri.parse(url).replace(queryParameters: _stringifyValue(query ?? {}));
    Future<Response> requestFuture = libHttp.post(uri, headers: mergedHeaders, body: body);
    Response response = await _requestWrapper(requestFuture);
    if (wrap) {
      final data = _responseWrapper(response);
      return data;
    } else {
      return response as T;
    }
  }

  Map<String, String> _mergeHeaders(headers) => Map<String, String>.from(headers ?? {})..addAll(commonHeaders);

  Map<String, dynamic> _stringifyValue(Map<String, dynamic> map) =>
      map.map((key, value) => MapEntry(key, value?.toString() ?? ''));

  Future<Response> _requestWrapper(Future<Response> future) async {
    Completer<Response> completer = Completer<Response>();
    final timer = Timer(connectionTimeout, () {
      if (!completer.isCompleted) completer.completeError(const SocketException('request read timeout'));
    });
    try {
      Response response = await future;
      timer.cancel();
      if (!completer.isCompleted) completer.complete(response);
    } catch (e, s) {
      JrLogger.e('request error: $e, $s', tag: TAG);
    }
    return completer.future;
  }

  T _responseWrapper<T>(Response response) {
    String? body;
    try {
      body = response.body;
    } catch (e) {}
    int statusCode = response.statusCode;
    if (statusCode < 0 || statusCode > 400) {
      throw JrHttpException('Request error with status code: $statusCode', body);
    }
    Map map;
    try {
      map = json.decode(response.body);
    } catch (e) {
      throw JrHttpJsonException('Response body decode error: $e', body);
    }

    int? code = map['code'];
    if (code == null || code != 0) {
      throw JrHttpBusinessException('Request failed with business code: $code', body);
    }

    T? data = map['data'];
    if (data == null) {
      throw JrHttpBusinessException('Request failed with null data', body);
    }
    return data;
  }

  Map<String, String> get commonHeaders => {
        'device-id': JrDevice.deviceId,
        'platform': JrDevice.platform,
        'model': JrDevice.model,
        'pkg': JrDevice.packageName,
        'ver': JrDevice.version,
        'p_ver': JrDevice.build,
        'kst': '1',
        'sys_lan': JrDevice.language,
        'lang': JrDevice.language,
        if (accessToken != null) 'Authorization': 'Bearer$accessToken',
      };
}

class JrHttpException implements Exception {
  final dynamic body;
  final dynamic message;

  JrHttpException([this.message, this.body]);

  String? get bodyMsg => bodyJson?['msg'];

  Map? get bodyJson {
    if (body is Map) {
      return body;
    }
    if (body is String) {
      try {
        return json.decode(body);
      } catch (e) {
        //
      }
    }
    return null;
  }

  @override
  String toString() => "JrHttpException: $message\n$body\n";
}

class JrHttpJsonException extends JrHttpException {
  JrHttpJsonException([super.message, super.body]);

  @override
  String toString() => "JrHttpJsonException: $message\n$body\n";
}

class JrHttpBusinessException extends JrHttpException {
  JrHttpBusinessException([super.message, super.body]);

  @override
  String toString() => "JrHttpBusinessException: $message\n$body\n";
}
